//go:build sql_integration

package requestmgr

import (
	"context"
	"fmt"
	"testing"
	"time"

	imageCVEDS "github.com/stackrox/rox/central/cve/image/v2/datastore"
	deploymentMockDS "github.com/stackrox/rox/central/deployment/datastore/mocks"
	imageDS "github.com/stackrox/rox/central/image/datastore"
	imageV2DS "github.com/stackrox/rox/central/imagev2/datastore"
	reprocessorMocks "github.com/stackrox/rox/central/reprocessor/mocks"
	sensorConnMgrMocks "github.com/stackrox/rox/central/sensor/service/connection/mocks"
	views "github.com/stackrox/rox/central/views/imagecve"
	vulnReqCache "github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/cache"
	"github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/common"
	vulnReqDS "github.com/stackrox/rox/central/vulnmgmt/vulnerabilityrequest/datastore"
	v1 "github.com/stackrox/rox/generated/api/v1"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/concurrency"
	"github.com/stackrox/rox/pkg/features"
	"github.com/stackrox/rox/pkg/fixtures"
	imageUtils "github.com/stackrox/rox/pkg/images/utils"
	"github.com/stackrox/rox/pkg/postgres/pgtest"
	"github.com/stackrox/rox/pkg/protocompat"
	"github.com/stackrox/rox/pkg/protoconv"
	"github.com/stackrox/rox/pkg/sac"
	"github.com/stackrox/rox/pkg/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"go.uber.org/mock/gomock"
	"golang.org/x/sync/semaphore"
)

const (
	imageOS = "windows-me"
)

var (
	allAllowedCtx             = sac.WithAllAccess(context.Background())
	expiryLoopDurationForTest = 5 * time.Second // use a much quicker loop for testing purposes
)

func TestVulnRequestManagerRevertExceptionWithUnifiedDeferral(t *testing.T) {
	suite.Run(t, new(VulnRequestManagerRevertExceptionTestSuite))
}

type VulnRequestManagerRevertExceptionTestSuite struct {
	mockCtrl *gomock.Controller
	suite.Suite

	ctx    context.Context
	testDB *pgtest.TestPostgres

	vulnReqDataStore   vulnReqDS.DataStore
	deployments        *deploymentMockDS.MockDataStore
	imageDataStore     imageDS.DataStore
	imageV2DataStore   imageV2DS.DataStore
	imageCVEDataStore  imageCVEDS.DataStore
	sensorConnMgrMocks *sensorConnMgrMocks.MockManager
	reprocessor        *reprocessorMocks.MockLoop
	manager            *managerImpl
	pendingReqCache    vulnReqCache.VulnReqCache
	activeReqCache     vulnReqCache.VulnReqCache
}

func (s *VulnRequestManagerRevertExceptionTestSuite) SetupTest() {
	s.ctx = context.Background()
	s.mockCtrl = gomock.NewController(s.T())
	s.testDB = pgtest.ForT(s.T())

	s.pendingReqCache, s.activeReqCache = vulnReqCache.New(), vulnReqCache.New()
	s.setupDataStores(s.pendingReqCache, s.activeReqCache)
	cveView := views.NewCVEView(s.testDB.DB)

	s.deployments = deploymentMockDS.NewMockDataStore(s.mockCtrl)
	s.sensorConnMgrMocks = sensorConnMgrMocks.NewMockManager(s.mockCtrl)
	s.reprocessor = reprocessorMocks.NewMockLoop(s.mockCtrl)
	s.manager = &managerImpl{
		deployments:                             s.deployments,
		imageCVEs:                               s.imageCVEDataStore,
		vulnReqs:                                s.vulnReqDataStore,
		connManager:                             s.sensorConnMgrMocks,
		reprocessor:                             s.reprocessor,
		pendingReqCache:                         s.pendingReqCache,
		activeReqCache:                          s.activeReqCache,
		revertTimedDeferralsTickerDuration:      expiryLoopDurationForTest,
		revertFixableCVEDeferralsTickerDuration: expiryLoopDurationForTest,
		stopper:                                 concurrency.NewStopper(),
		upsertSem:                               semaphore.NewWeighted(1),
		imageCVEView:                            cveView,
	}
	s.manager.images = s.imageDataStore
	s.manager.imagesV2 = s.imageV2DataStore
}

func (s *VulnRequestManagerRevertExceptionTestSuite) setupDataStores(pendingReqCache vulnReqCache.VulnReqCache, activeReqCache vulnReqCache.VulnReqCache) {
	var err error
	if features.FlattenImageData.Enabled() {
		s.imageV2DataStore = imageV2DS.GetTestPostgresDataStore(s.T(), s.testDB.DB)
	} else {
		s.imageDataStore = imageDS.GetTestPostgresDataStore(s.T(), s.testDB.DB)
	}
	s.imageCVEDataStore = imageCVEDS.GetTestPostgresDataStore(s.T(), s.testDB.DB)
	s.vulnReqDataStore, err = vulnReqDS.GetTestPostgresDataStore(s.T(), s.testDB.DB, pendingReqCache, activeReqCache)
	s.Require().NoError(err)
}

func (s *VulnRequestManagerRevertExceptionTestSuite) TestReObserveExpiredDeferralsMarksAllAsInactive() {
	expiredInThePast := time.Now().Add(-96 * time.Hour)
	expiresInFuture := time.Now().Add(30 * 24 * time.Hour)

	fpRequest := fixtures.GetGlobalFPRequest("cve-a-b")
	fpRequest.Status = storage.RequestStatus_APPROVED
	fpRequest.Comments = []*storage.RequestComment{} // clear out the comment to make testing the one added by expiry easier

	cases := []struct {
		name             string
		vulnRequest      *storage.VulnerabilityRequest
		shouldBeActive   bool
		shouldGetComment bool
	}{
		{
			name:             "Active and approved deferral with expiry in the past should be marked inactive with comment",
			vulnRequest:      newDeferral("req-active-def", false, storage.RequestStatus_APPROVED, expiredInThePast),
			shouldBeActive:   false,
			shouldGetComment: true,
		},
		{
			name:             "Active and approved deferral with a pending request should still be inactive if expiry is in past",
			vulnRequest:      newDeferral("req-updated-def", false, storage.RequestStatus_APPROVED_PENDING_UPDATE, expiredInThePast),
			shouldBeActive:   false,
			shouldGetComment: true,
		},
		{
			name:             "Inactive deferral should remain inactive but with no additional comment",
			vulnRequest:      newDeferral("req-inactive-def", true, storage.RequestStatus_APPROVED, expiredInThePast),
			shouldBeActive:   false,
			shouldGetComment: false,
		},
		{
			name:             "Pending deferral should not be marked as inactive",
			vulnRequest:      newDeferral("req-pending-def", false, storage.RequestStatus_PENDING, expiredInThePast),
			shouldBeActive:   true,
			shouldGetComment: false,
		},
		{
			name:             "Denied deferral should not be marked as inactive",
			vulnRequest:      newDeferral("req-denied-def", false, storage.RequestStatus_DENIED, expiredInThePast),
			shouldBeActive:   true,
			shouldGetComment: false,
		},
		{
			name:             "Deferral with expiry in future should not be marked as inactive",
			vulnRequest:      newDeferral("req-unexpired-def", false, storage.RequestStatus_APPROVED, expiresInFuture),
			shouldBeActive:   true,
			shouldGetComment: false,
		},
		{
			name:             "Deferrals with expires when fixed should not be marked as inactive",
			vulnRequest:      newDeferralExpiresWhenFixable("req-whenfixed-def", false, storage.RequestStatus_APPROVED, nil, false, "req-whenfixed-def"),
			shouldBeActive:   true,
			shouldGetComment: false,
		},
		{
			name:             "False positive requests should not be marked as inactive",
			vulnRequest:      fpRequest,
			shouldBeActive:   true,
			shouldGetComment: false,
		},
	}
	for _, c := range cases {
		s.T().Run(c.name, func(t *testing.T) {
			err := s.vulnReqDataStore.AddRequest(allAllowedCtx, c.vulnRequest)
			assert.NoError(t, err)

			s.manager.revertPastDueDeferralExceptions()

			r, ok, err := s.vulnReqDataStore.Get(allAllowedCtx, c.vulnRequest.GetId())
			assert.NoError(t, err)
			assert.True(t, ok)
			assert.Equal(t, c.shouldBeActive, !r.GetExpired())

			if c.shouldGetComment {
				assert.Len(t, r.GetComments(), 1)
				assert.Equal(t, r.GetComments()[0].GetMessage(), "[System Generated] Request expired")
				assert.Nil(t, r.GetComments()[0].GetUser()) // system generated so no user identity
			} else {
				assert.Len(t, r.GetComments(), 0)
			}
		})
	}
}

func (s *VulnRequestManagerRevertExceptionTestSuite) TestReObserveFixableDeferrals() {
	fixableMongo1 := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "latest", "sha256:SHA2", 2)
	fixableMongo2 := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "0.0.0.1", "sha256:sha3", 2)
	fixableNginx := getImageWithVulnerableComponents("stackrox.io", "srox/nginx", "latest", "sha256:SHAAAAAA", 2)

	// Both of these images have a different component but with the exact same CVE as the previous ones that is unfixable
	unfixableMongo := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "0.0.0.2", "sha256:sha4", 2)
	unfixableMongo.GetScan().GetComponents()[0].Version = "89.9"
	unfixableMongo.GetScan().GetComponents()[0].GetVulns()[0].SetFixedBy = nil
	unfixableMonitoring := getImageWithVulnerableComponents("stackrox.io", "stackrox/monitoring", "67.0", "sha256:SHBBBBBBB", 2)
	unfixableMonitoring.GetScan().GetComponents()[0].Version = "99.9"
	unfixableMonitoring.GetScan().GetComponents()[0].GetVulns()[0].SetFixedBy = nil

	if features.FlattenImageData.Enabled() {
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableMongo1)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableMongo2)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableNginx)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(unfixableMongo)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(unfixableMonitoring)))
	} else {
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableMongo1))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableMongo2))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableNginx))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, unfixableMongo))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, unfixableMonitoring))
	}

	fixableCVE1 := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[0].GetCve()
	unfixableCVE := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[2].GetCve()

	timedDeferral := newDeferral("timed-deferral", false, storage.RequestStatus_DENIED, time.Now().Add(1*time.Hour), fixableCVE1)
	fixableDeferralPendingUpdate := newDeferralExpiresWhenFixable("fixable-deferral-pending-update", false, storage.RequestStatus_APPROVED, nil, false, fixableCVE1)
	fixableDeferralPendingUpdate.UpdatedReq = getDeferralExpiryTimeUpdate(1 * time.Hour)
	timedDeferralPendingUpdate := newDeferral("timed-deferral-pending-update", false, storage.RequestStatus_APPROVED_PENDING_UPDATE, time.Now().Add(30*24*time.Hour), fixableCVE1)
	timedDeferralPendingUpdate.UpdatedReq = getDeferralExpiryFixableUpdate(true)

	shouldExpireReqs := []*storage.VulnerabilityRequest{
		// Deferral on a specific image for a fixable cve -> expire
		newDeferralExpiresWhenFixable("specific-image-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableCVE1),
		// Deferral globally for a fixable cve -> expire
		newDeferralExpiresWhenFixable("global-image-fixable", false, storage.RequestStatus_APPROVED, nil, false, fixableCVE1),
		// Deferral for all tags of an image for a fixable cve -> expire
		newDeferralExpiresWhenFixable("all-tags-image-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true), false, fixableCVE1),
		// Deferral for an image where diff components have the same fixable vuln -> expire
		newDeferralExpiresWhenFixable("multi-components-same-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableMongo1.GetScan().GetComponents()[0].GetVulns()[5].GetCve()),
		// Deferral for an image where both components have the same vuln but is fixable only in one -> expire
		newDeferralExpiresWhenFixable("multi-components-only-one-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableMongo1.GetScan().GetComponents()[0].GetVulns()[6].GetCve()),
		// When fixable deferral for fixable CVE that is pending update to a timed deferral -> expire
		fixableDeferralPendingUpdate,
	}

	shouldNotExpireReqs := []*storage.VulnerabilityRequest{
		// Deferral on a specific image for an unfixable cve -> DON'T expire
		newDeferralExpiresWhenFixable("specific-image-unfixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, unfixableCVE),
		// Deferral globally for an unfixable cve-> DON'T expire
		newDeferralExpiresWhenFixable("global-image-unfixable", false, storage.RequestStatus_APPROVED, nil, false, unfixableCVE),
		// Deferral for all tags of an image for an unfixable cve-> DON'T expire
		newDeferralExpiresWhenFixable("all-tags-image-unfixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true), false, unfixableCVE),
		// FP globally -> DON'T expire
		newFalsePositive("global-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, nil),
		// FP specific image -> DON'T expire
		newFalsePositive("specific-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false)),
		// FP all tags -> DON'T expire
		newFalsePositive("all-tags-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true)),
		// Denied deferral -> DON'T expire
		newDeferralExpiresWhenFixable("denied-deferral", false, storage.RequestStatus_DENIED, vulnScopeFromImage(fixableMongo1, false), false, fixableCVE1),
		// Timed deferral for fixable CVE -> DON'T expire
		timedDeferral,
		// Timed deferral for fixable CVE that is pending update to when fixable -> DON'T expire
		timedDeferralPendingUpdate,
	}

	for _, req := range append(shouldExpireReqs, shouldNotExpireReqs...) {
		err := s.vulnReqDataStore.AddRequest(allAllowedCtx, req)
		s.NoError(err)
	}

	s.sensorConnMgrMocks.EXPECT().BroadcastMessage(gomock.Any()).AnyTimes()
	s.deployments.EXPECT().SearchDeployments(gomock.Any(), gomock.Any()).Return([]*v1.SearchResult{}, nil).AnyTimes()
	s.reprocessor.EXPECT().ReprocessRiskForDeployments(gomock.Any()).AnyTimes()

	s.manager.revertFixableCVEDeferralExceptions()

	for _, req := range shouldExpireReqs {
		s.T().Run(req.GetId()+" - should expire", func(t *testing.T) {
			r, _, err := s.vulnReqDataStore.Get(allAllowedCtx, req.GetId())
			assert.NoError(t, err)
			assert.NotNil(t, r)
			assert.Truef(t, r.GetExpired(), req.GetId())

			assert.Len(t, r.GetComments(), 1)
			assert.Equal(t, r.GetComments()[0].GetMessage(), "[System Generated] Request expired")
			assert.Nil(t, r.GetComments()[0].GetUser()) // system generated so no user identity
		})
	}

	for _, req := range shouldNotExpireReqs {
		s.T().Run(req.GetId()+" - should not expire", func(t *testing.T) {
			r, _, err := s.vulnReqDataStore.Get(allAllowedCtx, req.GetId())
			assert.NoError(t, err)
			assert.NotNil(t, r)
			assert.False(t, r.GetExpired(), req.GetId())
			assert.Len(t, r.GetComments(), 0)
		})
	}
}

func (s *VulnRequestManagerRevertExceptionTestSuite) TestReObserveFixableDeferralsBulkCVEs() {
	fixableMongo1 := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "latest", "sha256:SHA2", 3)
	fixableMongo2 := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "0.0.0.1", "sha256:sha3", 3)
	fixableNginx := getImageWithVulnerableComponents("stackrox.io", "srox/nginx", "latest", "sha256:SHAAAAAA", 3)

	// Both of these images have a different component but with the exact same CVE as the previous ones that is unfixable
	unfixableMongo := getImageWithVulnerableComponents("stackrox.io", "srox/mongo", "0.0.0.2", "sha256:sha4", 3)
	unfixableMongo.GetScan().GetComponents()[0].Version = "89.9"
	unfixableMongo.GetScan().GetComponents()[0].GetVulns()[2].SetFixedBy = nil
	unfixableMonitoring := getImageWithVulnerableComponents("stackrox.io", "stackrox/monitoring", "67.0", "sha256:SHBBBBBBB", 3)
	unfixableMonitoring.GetScan().GetComponents()[0].Version = "99.9"
	unfixableMonitoring.GetScan().GetComponents()[0].GetVulns()[2].SetFixedBy = nil

	if features.FlattenImageData.Enabled() {
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableMongo1)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableMongo2)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(fixableNginx)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(unfixableMongo)))
		s.NoError(s.imageV2DataStore.UpsertImage(allAllowedCtx, imageUtils.ConvertToV2(unfixableMonitoring)))
	} else {
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableMongo1))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableMongo2))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, fixableNginx))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, unfixableMongo))
		s.NoError(s.imageDataStore.UpsertImage(allAllowedCtx, unfixableMonitoring))
	}

	// Fixable globally.
	fixableCVE1 := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[0].GetCve()
	// Fixable globally.
	fixableCVE2 := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[1].GetCve()
	// Fixable in a few images.
	fixableCVE3 := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[2].GetCve()
	unfixableCVE := fixableMongo1.GetScan().GetComponents()[0].GetVulns()[3].GetCve()

	timedDeferral := newDeferral("timed-deferral", false, storage.RequestStatus_DENIED, time.Now().Add(1*time.Hour), fixableCVE1, fixableCVE2)
	fixableDeferralPendingUpdate := newDeferralExpiresWhenFixable("fixable-deferral-pending-update", false, storage.RequestStatus_APPROVED, nil, false, fixableCVE1, fixableCVE2)
	fixableDeferralPendingUpdate.UpdatedReq = getDeferralExpiryTimeUpdate(1 * time.Hour)
	timedDeferralPendingUpdate := newDeferral("timed-deferral-pending-update", false, storage.RequestStatus_APPROVED_PENDING_UPDATE, time.Now().Add(30*24*time.Hour), fixableCVE1)
	timedDeferralPendingUpdate.UpdatedReq = getDeferralExpiryFixableUpdate(true)

	shouldExpireReqs := []*storage.VulnerabilityRequest{
		// Deferral on a specific image for a fixable cve and at least one CVE must be fixable -> expire
		newDeferralExpiresWhenFixable("specific-image-fixable-any-cve-fixable-expiry", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableCVE1, fixableCVE2),
		// Deferral for all tags of an image for a fixable cve and at least one CVE must be fixable -> expire
		newDeferralExpiresWhenFixable("all-tags-image-fixable-any-cve-fixable-expiry", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true), false, fixableCVE1, unfixableCVE),
		// Deferral for an image where diff components have the same fixable vuln -> expire
		newDeferralExpiresWhenFixable("multi-components-same-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableMongo1.GetScan().GetComponents()[0].GetVulns()[5].GetCve()),
		// Deferral for an image where both components have the same vuln but is fixable only in one -> expire
		newDeferralExpiresWhenFixable("multi-components-only-one-fixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), false, fixableMongo1.GetScan().GetComponents()[0].GetVulns()[6].GetCve()),
		// When fixable deferral for fixable CVE that is pending update to a timed deferral -> expire
		fixableDeferralPendingUpdate,
	}

	shouldNotExpireReqs := []*storage.VulnerabilityRequest{
		// Deferral on a specific image for an unfixable cve -> DON'T expire
		newDeferralExpiresWhenFixable("specific-image-unfixable-all-cve-fixable-expiry", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false), true, unfixableCVE),
		// Deferral globally for an unfixable cve-> DON'T expire
		newDeferralExpiresWhenFixable("global-image-unfixable-all-cve-fixable-expiry-1", false, storage.RequestStatus_APPROVED, nil, true, fixableCVE1, unfixableCVE),
		// Deferral globally for an unfixable cve-> DON'T expire
		newDeferralExpiresWhenFixable("global-image-unfixable-all-cve-fixable-expiry-2", false, storage.RequestStatus_APPROVED, nil, true, fixableCVE3, unfixableCVE),
		// Deferral for all tags of an image for an unfixable cve-> DON'T expire
		newDeferralExpiresWhenFixable("all-tags-image-unfixable", false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true), false, unfixableCVE),
		// FP globally -> DON'T expire
		newFalsePositive("global-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, nil),
		// FP specific image -> DON'T expire
		newFalsePositive("specific-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, false)),
		// FP all tags -> DON'T expire
		newFalsePositive("all-tags-image-fp", fixableCVE1, false, storage.RequestStatus_APPROVED, vulnScopeFromImage(fixableMongo1, true)),
		// Denied deferral -> DON'T expire
		newDeferralExpiresWhenFixable("denied-deferral", false, storage.RequestStatus_DENIED, vulnScopeFromImage(fixableMongo1, false), false, fixableCVE1),
		// Timed deferral for fixable CVE -> DON'T expire
		timedDeferral,
		// Timed deferral for fixable CVE that is pending update to when fixable -> DON'T expire
		timedDeferralPendingUpdate,
	}

	globalScopeAllFixable := newDeferralExpiresWhenFixable("global-image-fixable-all-cve-fixable-expiry", false, storage.RequestStatus_APPROVED, nil, true, fixableCVE1, fixableCVE2)
	// Deferral globally for a fixable cve and all CVEs must be fixable -> expire
	shouldExpireReqs = append(shouldExpireReqs, globalScopeAllFixable)

	for _, req := range append(shouldExpireReqs, shouldNotExpireReqs...) {
		err := s.vulnReqDataStore.AddRequest(allAllowedCtx, req)
		s.NoError(err)
	}

	s.sensorConnMgrMocks.EXPECT().BroadcastMessage(gomock.Any()).AnyTimes()
	s.deployments.EXPECT().SearchDeployments(gomock.Any(), gomock.Any()).Return([]*v1.SearchResult{}, nil).AnyTimes()
	s.reprocessor.EXPECT().ReprocessRiskForDeployments(gomock.Any()).AnyTimes()

	s.manager.revertFixableCVEDeferralExceptions()

	for _, req := range shouldExpireReqs {
		s.T().Run(req.GetId()+" - should expire", func(t *testing.T) {
			r, _, err := s.vulnReqDataStore.Get(allAllowedCtx, req.GetId())
			assert.NoError(t, err)
			assert.NotNil(t, r)
			assert.Truef(t, r.GetExpired(), req.GetId())

			assert.Len(t, r.GetComments(), 1)
			assert.Equal(t, r.GetComments()[0].GetMessage(), "[System Generated] Request expired")
			assert.Nil(t, r.GetComments()[0].GetUser()) // system generated so no user identity
		})
	}

	for _, req := range shouldNotExpireReqs {
		s.T().Run(req.GetId()+" - should not expire", func(t *testing.T) {
			r, _, err := s.vulnReqDataStore.Get(allAllowedCtx, req.GetId())
			assert.NoError(t, err)
			assert.NotNil(t, r)
			assert.False(t, r.GetExpired(), req.GetId())
			assert.Len(t, r.GetComments(), 0)
		})
	}
}

//// start test utilities

func newDeferral(id string, expired bool, status storage.RequestStatus, expiry time.Time, cves ...string) *storage.VulnerabilityRequest {
	id = id + "-" + uuid.NewV4().String()
	ret := &storage.VulnerabilityRequest{
		Id:          id,
		Name:        id,
		Status:      status,
		Expired:     expired,
		TargetState: storage.VulnerabilityState_DEFERRED,
		Req: &storage.VulnerabilityRequest_DeferralReq{
			DeferralReq: &storage.DeferralRequest{
				Expiry: &storage.RequestExpiry{
					Expiry: &storage.RequestExpiry_ExpiresOn{
						ExpiresOn: protocompat.ConvertTimeToTimestampOrNil(&expiry),
					},
				},
			},
		},
		Scope: &storage.VulnerabilityRequest_Scope{
			Info: &storage.VulnerabilityRequest_Scope_GlobalScope{
				GlobalScope: &storage.VulnerabilityRequest_Scope_Global{},
			},
		},
	}
	if len(cves) > 0 {
		ret.Entities = &storage.VulnerabilityRequest_Cves{
			Cves: &storage.VulnerabilityRequest_CVEs{
				Cves: cves,
			},
		}
	}
	return ret
}

func newDeferralExpiresWhenFixable(id string, expired bool, status storage.RequestStatus, imgScope *storage.VulnerabilityRequest_Scope, allFixable bool, cves ...string) *storage.VulnerabilityRequest {
	req := &storage.VulnerabilityRequest{
		Id:          id,
		Name:        id,
		Status:      status,
		Expired:     expired,
		TargetState: storage.VulnerabilityState_DEFERRED,
		Req: &storage.VulnerabilityRequest_DeferralReq{
			DeferralReq: &storage.DeferralRequest{
				Expiry: &storage.RequestExpiry{
					Expiry: &storage.RequestExpiry_ExpiresWhenFixed{
						// In v1, only any fixable was supported.
						ExpiresWhenFixed: !allFixable,
					},
					ExpiryType: func() storage.RequestExpiry_ExpiryType {
						if allFixable {
							return storage.RequestExpiry_ALL_CVE_FIXABLE
						}
						return storage.RequestExpiry_ANY_CVE_FIXABLE
					}(),
				},
			},
		},
		Scope: func() *storage.VulnerabilityRequest_Scope {
			return &storage.VulnerabilityRequest_Scope{
				Info: &storage.VulnerabilityRequest_Scope_ImageScope{
					ImageScope: &storage.VulnerabilityRequest_Scope_Image{
						Registry: common.MatchAll,
						Remote:   common.MatchAll,
						Tag:      common.MatchAll,
					},
				},
			}
		}(),
		Entities: &storage.VulnerabilityRequest_Cves{
			Cves: &storage.VulnerabilityRequest_CVEs{
				Cves: cves,
			},
		},
	}
	if imgScope != nil {
		req.Scope = imgScope
	}
	return req
}

func newFalsePositive(id, cve string, expired bool, status storage.RequestStatus, imgScope *storage.VulnerabilityRequest_Scope) *storage.VulnerabilityRequest {
	req := &storage.VulnerabilityRequest{
		Id:          id,
		Name:        id,
		Status:      status,
		Expired:     expired,
		TargetState: storage.VulnerabilityState_FALSE_POSITIVE,
		Req: &storage.VulnerabilityRequest_FpRequest{
			FpRequest: &storage.FalsePositiveRequest{},
		},
		Scope: &storage.VulnerabilityRequest_Scope{
			Info: &storage.VulnerabilityRequest_Scope_GlobalScope{
				GlobalScope: &storage.VulnerabilityRequest_Scope_Global{},
			},
		},
		Entities: &storage.VulnerabilityRequest_Cves{
			Cves: &storage.VulnerabilityRequest_CVEs{
				Cves: []string{cve},
			},
		},
	}
	if imgScope != nil {
		req.Scope = imgScope
	}
	return req
}

func vulnScopeFromImage(img *storage.Image, allTags bool) *storage.VulnerabilityRequest_Scope {
	scope := &storage.VulnerabilityRequest_Scope{
		Info: &storage.VulnerabilityRequest_Scope_ImageScope{
			ImageScope: &storage.VulnerabilityRequest_Scope_Image{
				Registry: img.GetName().GetRegistry(),
				Remote:   img.GetName().GetRemote(),
				Tag:      img.GetName().GetTag(),
			},
		},
	}
	if allTags {
		scope.GetImageScope().Tag = common.MatchAll
	}
	return scope
}

func getDeferralExpiryTimeUpdate(addHrToNow time.Duration) *storage.VulnerabilityRequest_UpdatedDeferralReq {
	return &storage.VulnerabilityRequest_UpdatedDeferralReq{
		UpdatedDeferralReq: &storage.DeferralRequest{
			Expiry: &storage.RequestExpiry{
				Expiry: &storage.RequestExpiry_ExpiresOn{
					ExpiresOn: protoconv.ConvertTimeToTimestamp(time.Now().Add(addHrToNow)),
				},
				ExpiryType: storage.RequestExpiry_TIME,
			},
		},
	}
}

func getDeferralExpiryFixableUpdate(anyFixable bool) *storage.VulnerabilityRequest_UpdatedDeferralReq {
	return &storage.VulnerabilityRequest_UpdatedDeferralReq{
		UpdatedDeferralReq: &storage.DeferralRequest{
			Expiry: &storage.RequestExpiry{
				Expiry: &storage.RequestExpiry_ExpiresWhenFixed{
					ExpiresWhenFixed: anyFixable,
				},
				ExpiryType: func() storage.RequestExpiry_ExpiryType {
					if anyFixable {
						return storage.RequestExpiry_ANY_CVE_FIXABLE
					}
					return storage.RequestExpiry_ALL_CVE_FIXABLE
				}(),
			},
		},
	}
}

func getImageWithVulnerableComponents(registry, remote, tag, id string, fixableCVECount int) *storage.Image {
	img := fixtures.GetImageWithUniqueComponents(5)
	img.Name = &storage.ImageName{
		Registry: registry,
		Remote:   remote,
		Tag:      tag,
		FullName: fmt.Sprintf("%s/%s:%s", registry, remote, tag),
	}
	img.Id = id
	img.GetScan().OperatingSystem = imageOS
	components := img.GetScan().GetComponents()

	// First two vulns are fixable, the rest are not
	for _, comp := range components {
		for i, vuln := range comp.GetVulns() {
			if i >= fixableCVECount {
				vuln.SetFixedBy = nil
			}
		}
	}

	// Add a new fixable vuln that's same across both components
	clonedVuln := components[0].GetVulns()[1].CloneVT()
	clonedVuln.Cve = "CVE-SAME-VULN"
	components[0].Vulns = append(components[0].Vulns, clonedVuln.CloneVT())
	components[1].Vulns = append(components[1].Vulns, clonedVuln.CloneVT())

	// Add another vuln that's same across both components but is fixable only for the 1st component
	clonedVuln = components[0].GetVulns()[1].CloneVT()
	clonedVuln.Cve = "CVE-SAME-VULN-2"
	components[0].Vulns = append(components[0].Vulns, clonedVuln.CloneVT())
	v := clonedVuln.CloneVT()
	v.SetFixedBy = nil
	components[1].Vulns = append(components[1].Vulns, v)

	return img
}

//// end test utilities
